Инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Заказчики ещё не знают, что это будет за место: кафе, ресторан, пиццерия, паб или бар, — и какими будут расположение, меню и цены.
Нам доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года. Информация, размещённая в сервисе Яндекс Бизнес, могла быть добавлена пользователями или найдена в общедоступных источниках. Она носит исключительно справочный характер.
Нам предстоят следующие шаги:
План работы:
Описание данных
name — название заведенияaddress — адрес заведенияcategory — категория заведенияhours — информация о днях и часах работыlat — широта географической точки, в которой находится заведениеlng — долгота географической точки, в которой находится заведениеrating — рейтинг заведения по оценкам пользователей в Яндекс Картахprice — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далееavg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):district — административный район, в котором находится заведениеseats — количество посадочных мест#импортируем библиотеки для работы
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.io as pio
!pip install plotly==5.16.1 #обновляем plotly, чтобы он не ругался на некоторые параметры графиков(иначе ругается)
!pip install kaleido
from IPython.display import Image
import json # подключаем модуль для работы с JSON-форматом
from plotly import graph_objects as go #круговая диаграмма, воронка продаж
from folium import Map, Marker # импортируем карту и маркер
from folium import Map, Choropleth # импортируем карту и хороплет
from folium.plugins import MarkerCluster # импортируем кластер
from folium.features import CustomIcon # импортируем собственные иконки
#устанавливаем единый стиль и палитру для графиков
sns.set_palette(['#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A', '#19D3F3', '#FF6692', '#B6E880', '#FF97FF', '#FECB52'])
sns.set_style('whitegrid')
#читаем файл с границами районов и сохраняем в переменной
with open('/datasets/admin_level_geomap.geojson', 'r') as f:
geo_json = json.load(f)
Requirement already satisfied: plotly==5.16.1 in /opt/conda/lib/python3.9/site-packages (5.16.1) Requirement already satisfied: packaging in /opt/conda/lib/python3.9/site-packages (from plotly==5.16.1) (21.3) Requirement already satisfied: tenacity>=6.2.0 in /opt/conda/lib/python3.9/site-packages (from plotly==5.16.1) (8.0.1) Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /opt/conda/lib/python3.9/site-packages (from packaging->plotly==5.16.1) (2.4.7) Requirement already satisfied: kaleido in /opt/conda/lib/python3.9/site-packages (0.2.1)
#считываем данные из csv-файла
df = pd.read_csv('/datasets/moscow_places.csv')
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
#выводим по нему информацию
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
По предварительной информации о датасете мы можем увидеть:
object, float и int#выведем статистику по данным типа object
df.describe(include=['object'])
| name | category | address | district | hours | price | avg_bill | |
|---|---|---|---|---|---|---|---|
| count | 8406 | 8406 | 8406 | 8406 | 7870 | 3315 | 3816 |
| unique | 5614 | 8 | 5753 | 9 | 1307 | 4 | 897 |
| top | Кафе | кафе | Москва, проспект Вернадского, 86В | Центральный административный округ | ежедневно, 10:00–22:00 | средние | Средний счёт:1000–1500 ₽ |
| freq | 189 | 2378 | 28 | 2242 | 759 | 2117 | 241 |
name и address, последнее может быть объяснено, если кафе и ресторан, например, находятся в одном здании, или заведения находятся в рамках одного фудкорта. hours, price и avg_bill. Последние могут быть объяснены отсутствием данных, но пропуски в первом из перечисленных столбцов вызывают вопросы: заведения не открылись или работают круглосуточно? Пропусков в hours более 6%.#выведем статистику по числовым значениям
df.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| lat | 8406.0 | 55.750109 | 0.069658 | 55.573942 | 55.705155 | 55.753425 | 55.795041 | 55.928943 |
| lng | 8406.0 | 37.608570 | 0.098597 | 37.355651 | 37.538583 | 37.605246 | 37.664792 | 37.874466 |
| rating | 8406.0 | 4.229895 | 0.470348 | 1.000000 | 4.100000 | 4.300000 | 4.400000 | 5.000000 |
| middle_avg_bill | 3149.0 | 958.053668 | 1009.732845 | 0.000000 | 375.000000 | 750.000000 | 1250.000000 | 35000.000000 |
| middle_coffee_cup | 535.0 | 174.721495 | 88.951103 | 60.000000 | 124.500000 | 169.000000 | 225.000000 | 1568.000000 |
| chain | 8406.0 | 0.381275 | 0.485729 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 1.000000 |
| seats | 4795.0 | 108.421689 | 122.833396 | 0.000000 | 40.000000 | 75.000000 | 140.000000 | 1288.000000 |
#выведем долю пропусков
df.isna().mean().sort_values(ascending=False)
middle_coffee_cup 0.936355 middle_avg_bill 0.625387 price 0.605639 avg_bill 0.546039 seats 0.429574 hours 0.063764 name 0.000000 category 0.000000 address 0.000000 district 0.000000 lat 0.000000 lng 0.000000 rating 0.000000 chain 0.000000 dtype: float64
middle_avg_bill и middle_coffee_cup зависят от данных в столбце avg_bill - они ожидаемы.chain информацию можно изменить на более понятную.chain¶#заменяем значения в новом столбце
df['chain_new'] = 'Несетевое заведение'
df.loc[df['chain'] == 1, 'chain_new'] = 'Сетевое заведение'
#возвращаем значения в изначальный
df['chain'] = df['chain_new']
#удаляем вспомогательный столбец
df = df.drop(columns = ['chain_new'], axis = 1)
#проверяем получившийся датасет
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | Несетевое заведение | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | Несетевое заведение | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | Несетевое заведение | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | Сетевое заведение | 148.0 |
Теперь данные в столбце chain стали более наглядными.
Пропуски есть в столбцах:
hours — информация о днях и часах работыprice — категория цен в заведенииavg_bill — средняя стоимость заказа в виде диапазонаmiddle_avg_bill — средний чек - пропуски нормальныmiddle_coffee_cup — стоимость одной чашки капучино - пропуски нормальныseats — количество посадочных местhours¶#выявим категории заведений с пропусками во времени работы
df[df['hours'].isna()]['category'].unique()
array(['булочная', 'кафе', 'быстрое питание', 'бар,паб', 'пиццерия',
'ресторан', 'кофейня', 'столовая'], dtype=object)
Уникальных значений в столбце category — 8, в строках с пропусками о времени работы встречаются все эти категории заведений. Выявить какую-то закономерность не получится. Рассмотрим пропуски в hours нагляднее.
print('Количество пропусков в столбце "hours":', df['hours'].isna().sum())
df[df['hours'].isna()].head()
Количество пропусков в столбце "hours": 536
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 38 | Ижора | булочная | Москва, Ижорский проезд, 5А | Северный административный округ | NaN | 55.888366 | 37.514856 | 4.4 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 40 | Кафе | кафе | Москва, Ижорская улица, 18, стр. 1 | Северный административный округ | NaN | 55.895115 | 37.524902 | 3.7 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 44 | Кафетерий | кафе | Москва, Ангарская улица, 24А | Северный административный округ | NaN | 55.876289 | 37.519315 | 3.8 | NaN | NaN | NaN | NaN | Сетевое заведение | 8.0 |
| 56 | Рыба из тандыра | быстрое питание | Москва, Коровинское шоссе, 46, стр. 5 | Северный административный округ | NaN | 55.888010 | 37.515960 | 1.5 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 108 | Кафе | бар,паб | Москва, МКАД, 82-й километр, вл18 | Северо-Восточный административный округ | NaN | 55.908930 | 37.558777 | 4.2 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
Мы имеем 536 пропусков в данных на с сервисов Яндекс Карты и Яндекс Бизнес. Если проверить пропуски в столбце со временем работы заведений на других площадках, то можно найти недостающие данные, например:
Однако, такое заполнение пропусков нецелесообразно, учитывая их количество — это займёт слишком много времени, а также может не отражать действительность относительно остального датасета: мы работаем с данными за 2022 год, в то время как в открытых источниках можно найти актуальную информацию — и некоторые заведения могли изменить время работы за этот период или сменить место и название.
Заменим пропуски в столбце hours на unknown, чтобы мы смогли использовать эти строки при анализе в дальнейшем, однако смысловой нагрузки такая замена не понесёт.
df['hours'] = df['hours'].fillna('unknown')
print('Количество пропусков в столбце "hours":', df['hours'].isna().sum())
Количество пропусков в столбце "hours": 0
price¶#выявим категории заведений с пропусками в категориях цен
df[df['price'].isna()]['category'].unique()
array(['кафе', 'кофейня', 'булочная', 'быстрое питание', 'пиццерия',
'ресторан', 'бар,паб', 'столовая'], dtype=object)
Здесь тоже не наблюдается прямой зависимости между пропусками и категориями заведений.
print('Количество пропусков в столбце "price":', df['price'].isna().sum())
df[df['price'].isna()].head(5)
Количество пропусков в столбце "price": 5091
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | Несетевое заведение | NaN |
| 11 | Шашлык Шефф | кафе | Москва, улица Маршала Федоренко, 10с1 | Северный административный округ | ежедневно, 10:00–21:00 | 55.881770 | 37.492362 | 4.9 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 13 | Буханка | булочная | Москва, Базовская улица, 15, корп. 1 | Северный административный округ | ежедневно, 08:00–22:00 | 55.877007 | 37.504980 | 4.8 | NaN | NaN | NaN | NaN | Сетевое заведение | 180.0 |
| 19 | Пекарня | булочная | Москва, Ижорский проезд, 5 | Северный административный округ | ежедневно, круглосуточно | 55.887969 | 37.515688 | 4.4 | NaN | NaN | NaN | NaN | Сетевое заведение | NaN |
В столбце price пропусков ещё больше: 5091. Это более 60% датасета. Заменить данные вручную нельзя, это исказит результаты анализа по известным категориям цен в заведениях. Мы не будем производить замену в этом столбце, чтобы в дальнейшем можно было использовать имеющиеся данные для анализа без использования среза.
avg_bill¶#выявим категории заведений с пропусками в диапазонах средних чеков
df[df['avg_bill'].isna()]['category'].unique()
array(['кафе', 'пиццерия', 'булочная', 'кофейня', 'быстрое питание',
'ресторан', 'бар,паб', 'столовая'], dtype=object)
И снова у нас нет никакой взаимосвязи с категориями, другие столбцы для выявления зависимости с пропусками использовать нецелесообразно: остальные имеющиеся данные не могут быть связаны с пропусками в данных о диапазонах средних чеков. Рассмотрим пропуски детальнее, чтобы убедиться в этом.
print('Количество пропусков в столбце "avg_bill":', df['avg_bill'].isna().sum())
df[df['avg_bill'].isna()].head()
Количество пропусков в столбце "avg_bill": 4590
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 5 | Sergio Pizza | пиццерия | Москва, Ижорская улица, вл8Б | Северный административный округ | ежедневно, 10:00–23:00 | 55.888010 | 37.509573 | 4.6 | средние | NaN | NaN | NaN | Несетевое заведение | NaN |
| 11 | Шашлык Шефф | кафе | Москва, улица Маршала Федоренко, 10с1 | Северный административный округ | ежедневно, 10:00–21:00 | 55.881770 | 37.492362 | 4.9 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 13 | Буханка | булочная | Москва, Базовская улица, 15, корп. 1 | Северный административный округ | ежедневно, 08:00–22:00 | 55.877007 | 37.504980 | 4.8 | NaN | NaN | NaN | NaN | Сетевое заведение | 180.0 |
| 19 | Пекарня | булочная | Москва, Ижорский проезд, 5 | Северный административный округ | ежедневно, круглосуточно | 55.887969 | 37.515688 | 4.4 | NaN | NaN | NaN | NaN | Сетевое заведение | NaN |
Да, данных просто нет — и с этим придётся смириться. Заменять пропуски нецелесообразно, так как этот столбец мы не будем использовать для анализа.
Пропуски в столбцах middle_avg_bill и middle_coffee_cup напрямую связаны с avg_bill, данные в них отбирались по подстрокам «Средний счёт» и «Цена одной чашки капучино», потому отдельно изучать их не будем, как и заменять: столбцы имеют числовой формат и внесение нулевых значений исказит результаты, а замена на unknown изменит тип столбца.
seats¶print('Количество пропусков в столбце "seats":', df['seats'].isna().sum())
#выявим категории заведений с пропусками количества посадочных мест
df[df['seats'].isna()]['category'].unique()
Количество пропусков в столбце "seats": 3611
array(['кафе', 'кофейня', 'пиццерия', 'бар,паб', 'булочная', 'столовая',
'быстрое питание', 'ресторан'], dtype=object)
Даже некоторые рестораны не указывают эту информацию. Пропуски в количестве посадочных мест могут быть в любой категории заведений. Столбец содержит данные типа int — замена на 0 может исказить данные датасета. Проверим, в каких категориях нулевое количество посадочных мест, чтобы убедиться в этом.
#создаём новую переменную с датафреймом, чтобы не повредить основной
data = pd.read_csv('/datasets/moscow_places.csv')
#группируем по категориям без посадочных мест
test = data[data.seats == 0].groupby('category').seats.count().sort_values(ascending=False).reset_index()
#выводим таблицу
test
| category | seats | |
|---|---|---|
| 0 | кафе | 44 |
| 1 | кофейня | 24 |
| 2 | ресторан | 20 |
| 3 | быстрое питание | 18 |
| 4 | булочная | 11 |
| 5 | пиццерия | 10 |
| 6 | столовая | 5 |
| 7 | бар,паб | 4 |
Рестораны без посадочных мест не на последнем месте в получившемся списке. Рассмотрим соотношение на круговой диаграмме.
#создаём круговую диаграмму
fig = go.Figure(data=[go.Pie(labels=test['category'], values=test['seats'])])
#задаём описание
fig.update_layout(title='Распределение нулевых посадочных мест по заведениям', width=800, height=600,
annotations=[dict(x=1.12, y=1.05, text='Заведения', showarrow=False)])
#сохраняем график как изображение png для github
pio.write_image(fig, 'figure.png', width=800, height=600, scale=1)
#выводим круговую диаграмму
fig.show()
Проверим в отдельной переменной, как сильно изменится информация, если заменить пропуски на 0.
#заменяем пропуски на 0
data['seats'] = data['seats'].fillna(0)
#группируем по категориям заведения без посадочных мест
data = data[data.seats == 0].groupby('category').seats.count().sort_values(ascending=False).reset_index()
#создаём круговую диаграмму
fig = go.Figure(data=[go.Pie(labels=data['category'], values=data['seats'])])
#задаём описание
fig.update_layout(title='Распределение нулевых посадочных мест по заведениям с заменой пропусков', width=800, height=600,
annotations=[dict(x=1.12, y=1.05, text='Заведения', showarrow=False)])
#выводим круговую диаграмму
fig.show()
#выводим таблицу
data
| category | seats | |
|---|---|---|
| 0 | кафе | 1204 |
| 1 | ресторан | 793 |
| 2 | кофейня | 686 |
| 3 | бар,паб | 301 |
| 4 | быстрое питание | 272 |
| 5 | пиццерия | 216 |
| 6 | столовая | 156 |
| 7 | булочная | 119 |
Некоторые категории заведений поменялись местами при подобной замене. Это может существенно отразиться не только на нулевых значениях, но и на всём объеме данных по столбцу seats, потому заменять пропуски не будем.
Мы изучили пропуски в следующих столбцах:
unknownВо всех категориальных текстовых столбцах уникальных наименований меньше, чем строк в датасете. Мы можем изучить их подробнее, чтобы исключить дубликаты, которые могут повлиять на дальнейший анализ. Текстовые столбцы в датасете:
name — название заведения — повторяющие наименования могут быть у сетей или у заведений без ярковыраженной уникальности, например кафе с именем "Кафе" — проверить на аномалииaddress — адрес заведения — проверить на аномалииcategory — категория заведения — 8 уникальных значений мы рассматривали при изучении пропусков, в этих данных аномалий нетhours — информация о днях и часах работы — проверить на аномалииprice — категория цен в заведении — проверить на аномалииavg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона — проверить на аномалииdistrict — административный район, в котором находится заведение — проверить на аномалииТакже стоит проверить на аномалии столбец seats — количество посадочных мест
name¶print('Количество уникальных значений в столбце "name":', df['name'].nunique())
list(df['name'].sort_values().unique()[:10])
Количество уникальных значений в столбце "name": 5614
['#КешбэкКафе', '+39 Pizzeria Mozzarella bar', '1 Этаж', '1-я Креветочная', '10 Идеальных Пицц', '1001 Ночь', '100ловая', '100лоффка', '13', '13 Chef doner']
Аномалий в виде лишних пробелов в начале и конце наименований нет. Всё остальное может быть интерпретировано как креативность владельцев заведений, за исключением:
55.709201, 37.3922571у471320464957/12'9 Bar Coffe' - '9 Bar Coffee' — в этой паре очевидно, что пропущена e в окончании кофейни'Cofe Fest' - 'CofeFest' — разница в пробеле в названии (в оригинальной сети нет пробела)Cafe 13 - 'Cafe13' — разница в пробеле в названии (в московском кафе нет пробела)'Drive Cafe' - 'Drive Café' — апостроф над é'Bb Grill' - 'Bb grill' — разница в написании строчных и заглавных букв в названии'Corner cafe & kitchen' - 'Corner cafe&kitchen' — разница в разделителе &"It's СоТ - Кофейня" - "It's СоТ-Кофейня" — разница в разделителе -"Jeffrey's Coffeeshop" - 'Jeffrey’s Coffeeshop' — разница в разделителе ’'Free & Со' - 'Free&co' — разница в раскладке написанияВыясним, что кроется за числовыми названиями.
df.query('name in ["55.709201, 37.392257", "1у", "47","13","2046","495","7/12"]')
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 21 | 7/12 | кафе | Москва, Прибрежный проезд, 7 | Северный административный округ | ежедневно, 10:00–22:00 | 55.876805 | 37.464934 | 4.5 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 2163 | 47 | кофейня | Москва, Центральный административный округ, Кр... | Центральный административный округ | пн-пт 08:00–20:00; сб,вс 10:00–19:00 | 55.778830 | 37.645842 | 4.9 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 2460 | 495 | ресторан | Москва, Олимпийский проспект, 18/1 | Центральный административный округ | ежедневно, 08:00–23:00 | 55.785079 | 37.624066 | 4.8 | NaN | NaN | NaN | NaN | Несетевое заведение | 481.0 |
| 2946 | 13 | кафе | Москва, Краснобогатырская улица, 90, стр. 2 | Восточный административный округ | unknown | 55.800889 | 37.710556 | 3.0 | NaN | NaN | NaN | NaN | Несетевое заведение | 52.0 |
| 3881 | 2046 | кофейня | Москва, 1-й Неопалимовский переулок, 1/9 | Центральный административный округ | пн-пт 08:00–21:30; сб,вс 10:00–21:30 | 55.740755 | 37.583738 | 4.8 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 5875 | 55.709201, 37.392257 | кафе | Москва, Беловежская улица, 22 | Западный административный округ | чт круглосуточно, перерыв 10:00–20:00; сб круг... | 55.709201 | 37.392257 | 4.2 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
| 8236 | 1у | кафе | Москва, Нагатинская набережная, 40/1к1 | Южный административный округ | unknown | 55.685528 | 37.673546 | 3.4 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN |
Следовательно, необходимо удалить строку с индексом 5875. А также, чтобы привести всё наименования к единому виду, воспользуемся заменой по найденным пунктам.
#удаляем строку по индексу
df = df.drop(index=5875)
#производим пошаговую замену
df['name'] = (df['name']
.str.replace('9 Bar Coffe','9 Bar Coffee', regex=True) #добавляем e к названию
.str.replace('9 Bar Coffeee','9 Bar Coffee', regex=True) #убираем лишнюю e
.str.replace('Cofe Fest','CofeFest', regex=True) #убираем пробел в названии
.str.replace('Cafe 13','Cafe13', regex=True) #убираем пробел в названии
.str.replace(' & ','&', regex=True) #делаем разделитель единообразным
.str.replace('&',' & ', regex=True) #возвращаем к разделителю пробелы
.str.replace(' - ',' ', regex=True) #заменим разделитель на пробел
.str.replace('-',' ', regex=True) #заменим разделитель на пробел
.str.replace('’',"'", regex=True) #заменяем написание апострофа
.str.replace('é',"e", regex=True) #удаляем буквенный апостроф
.str.lower() #приводим все символы к нижнему регистру
.str.replace('с','c', regex=True) #заменяем русскую раскладку на латиницу
.str.replace('о','o', regex=True) #заменяем русскую раскладку на латиницу
.str.replace('а','a', regex=True) #заменяем русскую раскладку на латиницу
.str.replace('е','e', regex=True) #заменяем русскую раскладку на латиницу
.str.title()) #приводим к единому написанию каждого слова с заглавной буквы
#проверяем результат
print('Количество уникальных значений в столбце "name":', df['name'].nunique())
list(df['name'].sort_values().unique()[:10])
Количество уникальных значений в столбце "name": 5478
['#Кeшбэккaфe', '+39 Pizzeria Mozzarella Bar', '1 Этaж', '1 Я Крeвeтoчнaя', '10 Идeaльных Пицц', '1001 Нoчь', '100Лoвaя', '100Лoффкa', '13', '13 Chef Doner']
Из датасета были удалены 135 неявных дубликата по столбцу name. А также одно заведение с некорректным названием.
address¶print('Количество уникальных значений в столбце "address":', df['address'].nunique())
list(df['address'].unique()[:10])
Количество уникальных значений в столбце "address": 5752
['Москва, улица Дыбенко, 7/1', 'Москва, улица Дыбенко, 36, корп. 1', 'Москва, Клязьминская улица, 15', 'Москва, улица Маршала Федоренко, 12', 'Москва, Правобережная улица, 1Б', 'Москва, Ижорская улица, вл8Б', 'Москва, Клязьминская улица, 9, стр. 3', 'Москва, Дмитровское шоссе, 107, корп. 4', 'Москва, Ангарская улица, 39', 'Москва, Левобережная улица, 12']
Среди выведенных адресов можно обратить внимание на следующие:
'Москва, парк культуры и отдыха Северное Тушино''Москва, Лианозовский парк культуры и отдыха''Москва, ландшафтный заказник Лианозовский''Москва, парк Этнографическая деревня Бибирево''Москва, Северный административный округ, район Левобережный, территория парка Дружбы''Москва, Северо-Восточный административный округ, район Отрадное''Москва, Северо-Восточный административный округ, Останкинский район, Выставка достижений народного хозяйства, Кольцевая дорога''Москва, Главный ботанический сад имени Н.В. Цицина Российской академии наук''Москва, Северо-Восточный административный округ, район Ростокино''Москва, Северо-Восточный административный округ, Останкинский район, Выставка достижений народного хозяйства, Главная аллея''Москва, Северо-Восточный административный округ, Останкинский район, территория парка Останкино, Останкинская аллея''Москва, Северо-Западный административный округ, район Строгино'Такие адреса нельзя назвать корректными, потому удалим из датасета строки, в которых в качестве адреса указывается административный округ Москвы или размытые адреса в стиле "парк", "ботанический сад" и подобные.
for value in df['address']:
if 'административный округ' in value:
df = df[df.address != value]
elif 'Главный ботанический сад имени Н.В. Цицина' in value:
df = df[df.address != value]
elif 'Этнографическая деревня Бибирево' in value:
df = df[df.address != value]
elif 'парк ' in value:
df = df[df.address != value]
elif 'ландшафтный заказник' in value:
df = df[df.address != value]
print('Количество уникальных значений в столбце "address":', df['address'].nunique())
df.info()
Количество уникальных значений в столбце "address": 5665 <class 'pandas.core.frame.DataFrame'> Int64Index: 8284 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8284 non-null object 1 category 8284 non-null object 2 address 8284 non-null object 3 district 8284 non-null object 4 hours 8284 non-null object 5 lat 8284 non-null float64 6 lng 8284 non-null float64 7 rating 8284 non-null float64 8 price 3301 non-null object 9 avg_bill 3799 non-null object 10 middle_avg_bill 3134 non-null float64 11 middle_coffee_cup 533 non-null float64 12 chain 8284 non-null object 13 seats 4795 non-null float64 dtypes: float64(6), object(8) memory usage: 970.8+ KB
Мы уже избавились от 122 строк. Также следует обратить внимание на то, что среди адресов есть строки, в которых перед наименованием улицы указан посёлок Рублёво. В дальнейшем эта информация нам может пригодиться при создании нового столбца.
Больше аномалий в названиях адреса нет. Проверим на дубликаты по адресу и наименованию — различия в других столбцах могут быть объяснимы, а нахождение одинаковых заведений в одном месте нужно рассмотреть.
#выведем дубликаты
df[df[['name','address']].duplicated(keep=False)]
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1430 | More Poke | ресторан | Москва, Волоколамское шоссе, 11, стр. 2 | Северный административный округ | ежедневно, 09:00–21:00 | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | Несетевое заведение | 188.0 |
| 1511 | More Poke | ресторан | Москва, Волоколамское шоссе, 11, стр. 2 | Северный административный округ | пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00... | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | Сетевое заведение | 188.0 |
| 2211 | Рaкoвaрня Клeшни И Хвocты | ресторан | Москва, проспект Мира, 118 | Северо-Восточный административный округ | ежедневно, 12:00–00:00 | 55.810553 | 37.638161 | 4.4 | NaN | NaN | NaN | NaN | Несетевое заведение | 150.0 |
| 2420 | Рaкoвaрня Клeшни И Хвocты | бар,паб | Москва, проспект Мира, 118 | Северо-Восточный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... | 55.810677 | 37.638379 | 4.4 | NaN | NaN | NaN | NaN | Сетевое заведение | 150.0 |
| 3091 | Хлeб Дa Выпeчкa | булочная | Москва, Ярцевская улица, 19 | Западный административный округ | ежедневно, 09:00–22:00 | 55.738886 | 37.411648 | 4.1 | NaN | NaN | NaN | NaN | Сетевое заведение | 276.0 |
| 3109 | Хлeб Дa Выпeчкa | кафе | Москва, Ярцевская улица, 19 | Западный административный округ | unknown | 55.738449 | 37.410937 | 4.1 | NaN | NaN | NaN | NaN | Несетевое заведение | 276.0 |
| 4613 | Cafe13 | кафе | Москва, Мясницкая улица, 13, стр. 11 | Центральный административный округ | пн-чт 10:00–22:00; пт 10:00–19:00 | 55.762779 | 37.633079 | 4.3 | NaN | NaN | NaN | NaN | Несетевое заведение | 200.0 |
| 4780 | Cafe13 | ресторан | Москва, Мясницкая улица, 13, стр. 11 | Центральный административный округ | пн-чт 10:00–22:00; пт 10:00–18:00 | 55.762748 | 37.633176 | 4.3 | NaN | NaN | NaN | NaN | Несетевое заведение | 200.0 |
Итого у нас 4 заведения в датасете повторяются:
More Poke совпадения не только по названию и адресу, но и по категории, различается время работы и сеть, судя по данным, ресторан входил в сеть, значит, нам нужно оставить строку с индексом 1511.Рaкoвaрня Клeшни И Хвocты является баром — пруфы, верный объект с индексом 2420.Хлeб Дa Выпeчкa является булочной — пруфы, значит, корректный объект с индексом 3091.Cafe13 является кафе — пруфы, нам нужно сохранить строку с индексом 4613.#удаляем строки по индексу
df = df.drop(index=[1430, 2211, 3109, 4780])
#проверяем результат
print('Количество дубликатов по столбцам "address" и "name":', df[['name','address']].duplicated().sum())
df.info()
Количество дубликатов по столбцам "address" и "name": 0 <class 'pandas.core.frame.DataFrame'> Int64Index: 8280 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8280 non-null object 1 category 8280 non-null object 2 address 8280 non-null object 3 district 8280 non-null object 4 hours 8280 non-null object 5 lat 8280 non-null float64 6 lng 8280 non-null float64 7 rating 8280 non-null float64 8 price 3301 non-null object 9 avg_bill 3799 non-null object 10 middle_avg_bill 3134 non-null float64 11 middle_coffee_cup 533 non-null float64 12 chain 8280 non-null object 13 seats 4791 non-null float64 dtypes: float64(6), object(8) memory usage: 970.3+ KB
В столбце address больше нет размытых адресов и дубликатов по заведениям. Датасет уменьшился на 1.5%.
hours¶print('Количество уникальных значений в столбце "hours":', df['hours'].nunique())
list(df['hours'].unique()[:10])
Количество уникальных значений в столбце "hours": 1292
['ежедневно, 10:00–22:00', 'пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00', 'ежедневно, 09:00–22:00', 'ежедневно, 10:00–23:00', 'пн 15:00–04:00; вт-вс 15:00–05:00', 'пн-чт 10:00–22:00; пт,сб 10:00–23:00; вс 10:00–22:00', 'ежедневно, 12:00–00:00', 'ежедневно, круглосуточно', 'ежедневно, 10:00–21:00', 'вт-сб 09:00–18:00']
Среди данных о времени работы заведений, кроме пропусков, аномалий нет, информация выглядит единообразно оформленной.
price¶print('Количество уникальных значений в столбце "price":', df['price'].nunique())
list(df['price'].unique())
Количество уникальных значений в столбце "price": 4
[nan, 'выше среднего', 'средние', 'высокие', 'низкие']
В категориях цен, исключая пропуски, аномалий нет.
avg_bill¶print('Количество уникальных значений в столбце "avg_bill":', df['avg_bill'].nunique())
list(df['avg_bill'].unique()[:10])
Количество уникальных значений в столбце "avg_bill": 896
[nan, 'Средний счёт:1500–1600 ₽', 'Средний счёт:от 1000 ₽', 'Цена чашки капучино:155–185 ₽', 'Средний счёт:400–600 ₽', 'Средний счёт:199 ₽', 'Средний счёт:200–300 ₽', 'Средний счёт:от 500 ₽', 'Средний счёт:1000–1200 ₽', 'Цена бокала пива:250–350 ₽']
В диапазонах среднего чека всё тоже выглядит весьма единообразно.
district¶print('Количество уникальных значений в столбце "district":', df['district'].nunique())
list(df['district'].unique())
Количество уникальных значений в столбце "district": 9
['Северный административный округ', 'Северо-Восточный административный округ', 'Северо-Западный административный округ', 'Западный административный округ', 'Центральный административный округ', 'Восточный административный округ', 'Юго-Восточный административный округ', 'Южный административный округ', 'Юго-Западный административный округ']
Среди округов тоже нет аномальных наименований. Но мы можем упростить этот столбцец, удалив повторяющийся префикс административный округ в новом столбце.
#удалим префиксы в новом столбце
df['area'] = df['district'].replace([' административный округ'], '', regex=True)
list(df['area'].unique())
['Северный', 'Северо-Восточный', 'Северо-Западный', 'Западный', 'Центральный', 'Восточный', 'Юго-Восточный', 'Южный', 'Юго-Западный']
Теперь наименования административных округов Москвы выглядят менее громоздко, что может пригодиться при создании графиков и таблиц.
seats¶Судя по выведенной ранее статистике, в датасете есть аномально большие значения в количестве посадочных мест. Они могут отличаться от категории к категории, потому выведем график размаха по разным категориям по отдельности.
#устанавливаем размер
plt.figure(figsize=(15, 7))
#строим диаграмму размаха
sns.boxplot(x='category', y='seats', data=df)
#присваиваем название графика и описание по осям
plt.title('Количество посадочных мест в разных категориях заведений', fontsize=18)
plt.xlabel('Категория заведений общественного питания', fontsize=14)
plt.ylabel('Количество подсадочных мест', fontsize=14)
plt.tight_layout()
plt.show()
Выбросы в каждой категории начинаются с разных значений, но объединяет их одно: после 700 мест выбросы становятся единичными. Ограничим график до этого значения, чтобы рассмотреть его поближе.
#устанавливаем размер
plt.figure(figsize=(15, 7))
#строим диаграмму размаха
sns.boxplot(x='category', y='seats', data=df.query('seats < 700'))
#присваиваем название графика и описание по осям
plt.title('Количество посадочных мест в разных категориях заведений', fontsize=18)
plt.xlabel('Категория заведений общественного питания', fontsize=14)
plt.ylabel('Количество подсадочных мест', fontsize=14)
plt.tight_layout()
plt.show()
Судя по диаграмме размаха, выбросов много, но аномальные и редкие выбросы начинаются в разных диапазонах, мы можем удалить значения в разных категориях до следующего уровня:
Важно помнить, что у нас есть пропуски в seats, а потому для удаления лишних значений воспользуемся вспомогательным столбцом, где пропуски заменим на ноль, а после устранения аномалий удалим его.
#обновляем индексацию датасета, создавая новый столбец с уникальными значениями
df = df.reset_index()
#заменяем пропуски в новом столбце на 0
df['new_seats'] = df['seats'].fillna(0)
#создаём сводные таблицы по разным категориям
debridment_1 = df.query(
'category in ["кафе","ресторан","кофейня","бар,паб"] and new_seats > 550').pivot_table(index='index')
debridment_2 = df.query(
'category in ["быстрое питание","булочная","столовая","пиццерия"] and new_seats > 400').pivot_table(index='index')
#удаляем аномальные выбросы с помощью индексов
df = df.query('index not in @debridment_1.index')
df = df.query('index not in @debridment_2.index')
#удаляем вспомогательные столбцы
df = df.drop(columns = ['index', 'new_seats'],axis = 1)
#обновляем индексы
df = df.reset_index(drop=True)
#проверяем оставшийся датасет
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8217 entries, 0 to 8216 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8217 non-null object 1 category 8217 non-null object 2 address 8217 non-null object 3 district 8217 non-null object 4 hours 8217 non-null object 5 lat 8217 non-null float64 6 lng 8217 non-null float64 7 rating 8217 non-null float64 8 price 3278 non-null object 9 avg_bill 3771 non-null object 10 middle_avg_bill 3112 non-null float64 11 middle_coffee_cup 527 non-null float64 12 chain 8217 non-null object 13 seats 4728 non-null float64 14 area 8217 non-null object dtypes: float64(6), object(9) memory usage: 963.1+ KB
Первоначальный датасет уменьшился на 2,2%, при этом мы избавиись от аномальных значений в количестве посадочных мест, от заведений с размытыми адресами, от дубликатов в адресах. Помимо этого мы устранили неявные дубликаты в названиях заведений и заведение с некорректным наименованием.
Также мы заменили пропуски в столбце hours с указанием времени работы заведения. Сохранены пропуски в пяти столбцах:
street¶#создаём функцию для выделения улицы
def street(address, sep=', '):
if 'посёлок' in address:
street = address.split(sep=sep)
return street[2]
else:
street = address.split(sep=sep)
return street[1]
#создаём столбец street
df['street'] = df['address'].apply(street)
#проверяем уникальные значения
print('Количество уникальных значений в столбце "street":', df['street'].nunique())
list(df['street'].unique()[:10])
Количество уникальных значений в столбце "street": 1408
['улица Дыбенко', 'Клязьминская улица', 'улица Маршала Федоренко', 'Правобережная улица', 'Ижорская улица', 'Дмитровское шоссе', 'Ангарская улица', 'Левобережная улица', 'МКАД', 'Базовская улица']
Все в сборе, даже улицы из посёлка Рублёво: Новолучанская улица и улица Василия Ботылёва.
is_24/7¶#создаём функцию для отделения круглосуточных заведений
def round_the_clock(hours):
if 'ежедневно, круглосуточно' in hours:
return True
else:
return False
#создаём столбец street
df['is_24/7'] = df['hours'].apply(round_the_clock)
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | area | street | is_24/7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Wowфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | Несетевое заведение | NaN | Северный | улица Дыбенко | False |
| 1 | Чeтырe Кoмнaты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | Несетевое заведение | 4.0 | Северный | улица Дыбенко | False |
| 2 | Хaзри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | Несетевое заведение | 45.0 | Северный | Клязьминская улица | False |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | Несетевое заведение | NaN | Северный | улица Маршала Федоренко | False |
| 4 | Иль Мaркo | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | Сетевое заведение | 148.0 | Северный | Правобережная улица | False |
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8217 entries, 0 to 8216 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8217 non-null object 1 category 8217 non-null object 2 address 8217 non-null object 3 district 8217 non-null object 4 hours 8217 non-null object 5 lat 8217 non-null float64 6 lng 8217 non-null float64 7 rating 8217 non-null float64 8 price 3278 non-null object 9 avg_bill 3771 non-null object 10 middle_avg_bill 3112 non-null float64 11 middle_coffee_cup 527 non-null float64 12 chain 8217 non-null object 13 seats 4728 non-null float64 14 area 8217 non-null object 15 street 8217 non-null object 16 is_24/7 8217 non-null bool dtypes: bool(1), float64(6), object(10) memory usage: 1.0+ MB
Добавлены новые столбцы:
area — укороченные наименования административных округов Москвы.street — названия улиц с типом object is_24/7 — заведения, работающие ежедневно и круглосуточно, с булевыми значениями#moscow_lat - широта центра москвы, moscow_lng - долгота центра москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#создаём карту москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
#создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(geo_data=geo_json,data=df,
columns=['district','rating'], key_on='feature.name',
fill_color='Greens', fill_opacity=0.01).add_to(m)
#создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)
def create_clusters(row):
icon_url = 'https://img.icons8.com/?size=512&id=30622&format=png'
icon = CustomIcon(icon_url, icon_size=(30, 30))
Marker(
[row['lat'], row['lng']],
popup=f"Название: {row['name']}; Категория: {row['category']}; Время работы: {row['hours']}; \
Рейтинг: {row['rating']}; Количество посадочных мест: {row['seats']}",
icon=icon,
).add_to(marker_cluster)
#применяем функцию для создания кластеров к каждой строке датафрейма
df.apply(create_clusters, axis=1)
#выводим карту
m